package cn.cosmosx.base.handler;

import cn.cosmosx.base.config.ServletErrorConfiguration;
import cn.cosmosx.base.constant.ResultCode;
import cn.cosmosx.base.define.ResultVO;
import cn.cosmosx.base.exception.BusinessException;
import cn.cosmosx.base.exception.PlatformException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 过滤器/servlet异常处理
 * <p>
 * 主要处理经过过滤器但尚未到达controller的异常
 * <p>
 * {@linkplain GlobalExceptionHandler} 全局异常处理没法处理过虑器中抛出的异常
 * 和执行顺序有关：
 * <p>
 * filter -> interceptor -> controllerAdvice -> aspect -> controller
 * <p>
 * 当controller返回异常时，也会按照controller -> aspect -> controllerAdvice -> interceptor -> filter来依次抛出
 * <p>
 * 注意：此方法不能直接在类上使用{@linkplain org.springframework.stereotype.Controller}
 * <p>
 * 或 {@linkplain org.springframework.web.bind.annotation.RestController} 标注，
 * <p>
 * 原因是{@linkplain ErrorProperties}和{@linkplain ErrorAttributes}无法注入。
 * <p>
 * 采用如同SpringBoot注入{@linkplain BasicErrorController}的注入方式一样，采用{@linkplain org.springframework.context.annotation.Bean}的方式注入。
 * <p>
 * 可参考{@linkplain ErrorMvcAutoConfiguration}
 * <p>
 * 该类在{@linkplain ServletErrorConfiguration}中注入spring容器
 */
@Slf4j
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ServletErrorHandler extends AbstractErrorController {
    private final ErrorProperties errorProperties;
    private final List<ErrorViewResolver> errorViewResolvers;

    public ServletErrorHandler(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes);
        this.errorProperties = serverProperties.getError();
        this.errorViewResolvers = errorViewResolvers;
    }

    /**
     * JSON异常返回
     */
    @ResponseStatus(value = HttpStatus.OK)
    @RequestMapping
    public ResponseEntity<ResultVO<?>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(ResultVO.failed(), HttpStatus.OK);
        }
        if (status == HttpStatus.NOT_FOUND) {
            return new ResponseEntity<>(ResultVO.failed(ResultCode.E, "请求的资源不存在"), HttpStatus.OK);
        }
        final Map<String, Object> body = getErrorAttributes(request, getAllErrorAttributeOptions());
        log.error("{}(servlet error):[path:{}, errorMessage:{}], {}", this.getClass().getName(), body.get("path"), body.get("error"), body.get("trace"));
        // 异常错误处理
        String exception = (String) body.get("exception");
        if (PlatformException.class.getTypeName().equals(exception) || BusinessException.class.getTypeName().equals(exception)) {
            return new ResponseEntity<>(ResultVO.failed(ResultCode.E, (String) body.get("message")), HttpStatus.OK);
        }
        return new ResponseEntity<>(ResultVO.failed(ResultCode.E, "网络繁忙，请稍后再试"), HttpStatus.OK);
    }

    /**
     * 普通异常返回
     */
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        HttpStatus status = getStatus(request);
        Map<String, Object> model = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML));
        Map<String, Object> map = new HashMap<>();
        map.put("title", status.getReasonPhrase() + "page");
        map.put("code", model.get("status"));
        map.put("message", model.get("error"));
        response.setStatus(status.value());
        log.error("{}", model);
        return new ModelAndView("error/error", map, HttpStatus.OK);
    }

    @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
    public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        return ResponseEntity.status(status).build();
    }

    protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
        ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
        if (this.errorProperties.isIncludeException()) {
            options = options.including(ErrorAttributeOptions.Include.EXCEPTION);
        }
        if (isIncludeStackTrace(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.STACK_TRACE);
        }
        if (isIncludeMessage(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.MESSAGE);
        }
        if (isIncludeBindingErrors(request, mediaType)) {
            options = options.including(ErrorAttributeOptions.Include.BINDING_ERRORS);
        }
        return options;
    }

    protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeStacktrace()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getTraceParameter(request);
            default:
                return false;
        }
    }

    protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeMessage()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getMessageParameter(request);
            default:
                return false;
        }
    }

    protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
        switch (getErrorProperties().getIncludeBindingErrors()) {
            case ALWAYS:
                return true;
            case ON_PARAM:
                return getErrorsParameter(request);
            default:
                return false;
        }
    }

    protected ErrorProperties getErrorProperties() {
        return this.errorProperties;
    }

    protected List<ErrorViewResolver> getErrorViewResolvers() {
        return this.errorViewResolvers;
    }

    protected ErrorAttributeOptions getAllErrorAttributeOptions() {
        return ErrorAttributeOptions.of(ErrorAttributeOptions.Include.EXCEPTION, ErrorAttributeOptions.Include.MESSAGE, ErrorAttributeOptions.Include.BINDING_ERRORS, ErrorAttributeOptions.Include.STACK_TRACE);
    }
}
